﻿using System;
using System.Diagnostics;
using System.Threading;
using Microsoft.Ccr.Core;
using Microsoft.Dss.ServiceModel.Dssp;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SoCGadget.Robotics.XBee;
using W3C.Soap;

namespace SoCGadget.XBeeServiceTest
{
    #region Extension Methods

    public static class Extensions
    {
        public static UInt64 Get64BitAddress(this XBeeAddress64 a)
        {
            return (UInt64)a.AddressHigh << 32 | a.AddressLow;
        }

        public static byte GetIoConfigByte(this DeviceOutput d)
        {
            return (byte)(d.OutputState ? 0x5 : 0x4);
        }
    }

    #endregion

    /// <summary>
    /// Summary description for UnitTest1
    /// </summary>
    [TestClass]
    public class XBeeServiceImplTest
    {
        private const UInt16 Address16Bit = 0xFFFE;
        private static Dispatcher _dispatcher;
        private static DispatcherQueue _dispatcherQueue;

        public XBeeServiceImplTest()
        {
        }

        private TestContext testContextInstance;

        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        #region Additional test attributes
        
        //Use ClassInitialize to run code before running the first test in the class
        [ClassInitialize()]
        public static void MyClassInitialize(TestContext testContext)
        {
            _dispatcher = new Dispatcher();
            _dispatcherQueue = new DispatcherQueue("XBee Service Impl Test", _dispatcher);
        }
        
        //Use ClassCleanup to run code after all tests in a class have run
        [ClassCleanup()]
        public static void MyClassCleanup()
        {
            _dispatcher.Dispose();
        }
        
        // Use TestInitialize to run code before running each test 
        // [TestInitialize()]
        // public void MyTestInitialize() { }
        //
        // Use TestCleanup to run code after each test has run
        // [TestCleanup()]
        // public void MyTestCleanup() { }
        //
        #endregion

        #region Test Helpers

        private DeviceMessage CreateDeviceMessage()
        {
            var dm = new DeviceMessage
                {
                    DestinationAddress = new XBeeAddress64
                    {
                        AddressHigh = 0x1234,
                        AddressLow = 0x4321
                    },
                    Message = "The message"
                };
            return dm;
        }

        private DeviceOutput CreateDeviceOutput(byte ioNum, bool state)
        {
            var d = new DeviceOutput
                {
                    DestinationAddress = new XBeeAddress64
                    {
                        AddressHigh = 0x1234,
                        AddressLow = 0x4321
                    },
                    OutputNumber = ioNum,
                    OutputState = state
                };
            return d;
        }

        #endregion

        [TestMethod]
        public void TestSendMessageToEndPointSuccess()
        {
            // arrange
            var xbModule = new Mock<IXBeeModule>();
            var serviceImpl = new XBeeServiceImpl(xbModule.Object);
            var deviceMsg = CreateDeviceMessage();
            var msg = new DeviceMessageUpdate(deviceMsg);

            xbModule.Setup(x => x.SendString(It.IsAny<ulong>(), Address16Bit, It.IsAny<string>(), It.IsAny<Action<CommandStatus>>()))
                .Callback((UInt64 p1, UInt16 p2, string p3, Action<CommandStatus> action) => action(CommandStatus.Success));

            // act
            serviceImpl.SendMessageToEndPoint(msg);

            // assert
            xbModule.Verify(x => x.SendString(deviceMsg.DestinationAddress.Get64BitAddress(), Address16Bit,
                deviceMsg.Message, It.IsAny<Action<CommandStatus>>()));

            var are = new AutoResetEvent(false);
            Arbiter.Activate(_dispatcherQueue,
                Arbiter.Receive(false, msg.ResponsePort.P0, response => are.Set())
            );
            Assert.IsTrue(are.WaitOne(1000), "Did not receive response from XBee service.");
        }

        [TestMethod]
        public void TestSendMessageToEndPointFail()
        {
            // arrange
            var xbModule = new Mock<IXBeeModule>();
            var serviceImpl = new XBeeServiceImpl(xbModule.Object);
            var deviceMsg = CreateDeviceMessage();
            var msg = new DeviceMessageUpdate(deviceMsg);

            xbModule.Setup(x => x.SendString(It.IsAny<ulong>(), Address16Bit, It.IsAny<string>(), It.IsAny<Action<CommandStatus>>()))
                .Callback((UInt64 p1, UInt16 p2, string p3, Action<CommandStatus> action) => action(CommandStatus.Error));

            // act
            serviceImpl.SendMessageToEndPoint(msg);

            // assert
            xbModule.Verify(x => x.SendString(deviceMsg.DestinationAddress.Get64BitAddress(), Address16Bit,
                deviceMsg.Message, It.IsAny<Action<CommandStatus>>()));

            var are = new AutoResetEvent(false);
            Arbiter.Activate(_dispatcherQueue,
                Arbiter.Receive(false, msg.ResponsePort.P1, response => are.Set())
            );
            Assert.IsTrue(are.WaitOne(1000), "Did not receive response from XBee service.");
        }

        [TestMethod]
        public void TestSendDigitalOutputToEndPointSuccess()
        {
            // arrange
            var xbModule = new Mock<IXBeeModule>();
            var serviceImpl = new XBeeServiceImpl(xbModule.Object);
            var deviceMsg = CreateDeviceOutput(0, true);
            var msg = new DeviceOutputUpdate(deviceMsg);

            xbModule.Setup(x =>
                x.SendRemoteCommand(It.IsAny<ulong>(), Address16Bit, It.IsAny<string>(), It.IsAny<byte>(), It.IsAny<Action<CommandStatus>>()))
                    .Callback((UInt64 p1, UInt16 p2, string p3, byte p4, Action<CommandStatus> action) =>
                        action(CommandStatus.Success));

            // act
            serviceImpl.SendDigitalOutputToEndPoint(msg);

            // assert
            xbModule.Verify(x =>
                x.SendRemoteCommand(deviceMsg.DestinationAddress.Get64BitAddress(), Address16Bit, "D0",
                    deviceMsg.GetIoConfigByte(), It.IsAny<Action<CommandStatus>>()));

            var are = new AutoResetEvent(false);
            Arbiter.Activate(_dispatcherQueue,
                Arbiter.Receive(false, msg.ResponsePort.P0, response => are.Set())
            );
            Assert.IsTrue(are.WaitOne(1000), "Did not receive response from XBee service.");
        }

        [TestMethod]
        public void TestSendDigitalOutputToEndPointFail()
        {
            // arrange
            var xbModule = new Mock<IXBeeModule>();
            var serviceImpl = new XBeeServiceImpl(xbModule.Object);
            var deviceMsg = CreateDeviceOutput(0, true);
            var msg = new DeviceOutputUpdate(deviceMsg);

            xbModule.Setup(x =>
                x.SendRemoteCommand(It.IsAny<ulong>(), Address16Bit, It.IsAny<string>(), It.IsAny<byte>(), It.IsAny<Action<CommandStatus>>()))
                    .Callback((UInt64 p1, UInt16 p2, string p3, byte p4, Action<CommandStatus> action) =>
                        action(CommandStatus.Error));

            // act
            serviceImpl.SendDigitalOutputToEndPoint(msg);

            // assert
            xbModule.Verify(x =>
                x.SendRemoteCommand(deviceMsg.DestinationAddress.Get64BitAddress(), Address16Bit, "D0",
                    deviceMsg.GetIoConfigByte(), It.IsAny<Action<CommandStatus>>()));

            var are = new AutoResetEvent(false);
            Arbiter.Activate(_dispatcherQueue,
                Arbiter.Receive(false, msg.ResponsePort.P1, response => are.Set())
            );
            Assert.IsTrue(are.WaitOne(1000), "Did not receive response from XBee service.");
        }

        [TestMethod]
        public void TestSendDigitalOutputToEndPointInvalidIo()
        {
            // arrange
            var xbModule = new Mock<IXBeeModule>();
            var serviceImpl = new XBeeServiceImpl(xbModule.Object);
            var deviceMsg = CreateDeviceOutput(8, true);
            var msg = new DeviceOutputUpdate(deviceMsg);

            xbModule.Setup(x =>
                x.SendRemoteCommand(It.IsAny<ulong>(), Address16Bit, It.IsAny<string>(), It.IsAny<byte>(), It.IsAny<Action<CommandStatus>>()))
                    .Callback((UInt64 p1, UInt16 p2, string p3, byte p4, Action<CommandStatus> action) =>
                        action(CommandStatus.Error));

            try
            {
                // act
                serviceImpl.SendDigitalOutputToEndPoint(msg);
                // assert
                Assert.Fail("No exception thrown for invalid argument.");
            }
            catch (ArgumentException)
            {
            }
            catch (Exception)
            {
                Assert.Fail("Wrong exception type thrown for invalid argument.");
            }
        }

        [TestMethod]
        public void TestDataSampleReceivedEvent()
        {
            // arrange
            var xbModule = new Mock<IXBeeModule>();
            var addr = new XBeeAddress64();
            addr.AddressHigh = 0x1234;
            addr.AddressLow = 0x4321;
            UInt16 dmask = 0x0F;
            UInt16 dsample = 0x0A;
            byte amask = 0x1;
            UInt16 asample = 0xDA;
            bool eventHandled = false;
            var serviceImpl = new XBeeServiceImpl(xbModule.Object);
            serviceImpl.DataSampleReceived += (sender, args) =>
                {
                    // assert
                    eventHandled = true;
                    Assert.AreEqual(args.SourceSerialNumber, addr.Get64BitAddress(), "Received incorrect SourceSerialNumber.");
                    Assert.AreEqual(args.SourceAddress, Address16Bit, "Received incorrect SourceAddress.");
                    Assert.AreEqual(args.DigitalChannelMask.Raw, dmask, "Received incorrect DigitalChannelMask");
                    Assert.AreEqual(args.DigitalIO.Raw, dsample, "Received incorrect DigitalIO");
                    Assert.AreEqual(args.AnalogChannelMask.Raw, amask, "Received incorrect AnalogChannelMask");
                    Assert.AreEqual(args.AnalogIO[0], asample, "Received incorrect AnalogIO");
                };

            // act - xbModule will raise a DataSampleReceived event, the serviceImpl will handle it, do some work,
            // and re-raise it. We will assert that the event was re-raised and args to the event are correct.
            xbModule.Raise(x => x.DataSampleReceived += null,
                new DataSampleReceivedArgs(addr.Get64BitAddress(), Address16Bit, dmask, dsample, amask, new UInt16[] { asample }));
            Assert.IsTrue(eventHandled, "DataSampleReceived event was not fired.");
        }
    }
}
